实战目标
将先前基于 Vite + Vue 3 的首页项目,通过 vite-plugin-ssr 插件改造为支持服务端渲染(SSR)的应用。这是从纯客户端渲染迈向 SSR 的第一个实战方案。
改造策略:初始化新项目 vs 直接改造
在动手之前,需要做一个关键决策:是在原有项目上直接改造,还是基于官方脚手架初始化新项目再迁移代码?
查看官方的 Vue + TypeScript 示例后会发现,项目结构发生了显著变化:
| 对比项 | 纯 Vite 项目 | vite-plugin-ssr 项目 |
|---|---|---|
| 入口文件 | main.ts | renderer/_default.page.server.ts + client.ts |
| 服务端 | 无 | Express 服务(server/index.ts) |
| 路由 | vue-router 手动配置 | 文件系统路由(pages/ 目录) |
| 构建配置 | vite.config.ts | 双端配置(client + server) |
由于结构差异较大,且官方文档并未提供详细的合并指南,推荐使用脚手架初始化新项目,再逐步迁移原有代码。
项目初始化
# 使用官方脚手架创建项目
npx create-vite-plugin-ssr@latest
# 安装依赖
pnpm install
# 启动开发服务器
npm run dev
bash
启动后访问 http://localhost:3000,即可看到第一个 SSR 页面。此时页面已具备路由和响应式特性。
项目结构解析
初始化完成的项目结构如下:
├── server/
│ └── index.ts # Express 服务端入口
├── renderer/
│ ├── _default.page.server.ts # 服务端渲染逻辑
│ ├── _default.page.client.ts # 客户端水合逻辑
│ ├── _app.ts # 应用入口(createApp)
│ └── PageShell.vue # 公共布局(Layout)
├── pages/
│ └── index/ # 文件路由目录
│ ├── index.page.vue
│ └── Counter.vue
└── package.json
text
服务端(server/index.ts)
使用 Express 作为 Node.js 端的 Web 框架,将 vite-plugin-ssr 作为中间件接入,负责读取页面路由并响应对应资源:
import express from 'express'
import { createServer as createViteServer } from 'vite'
import { serve } from 'vite-plugin-ssr/server'
const app = express()
// 开发环境下启动 Vite 开发服务器
const viteDevServer = await createViteServer({
server: { middlewareMode: true }
})
app.use(viteDevServer.middlewares)
// 注册 vite-plugin-ssr 中间件
app.use(serve({ root: process.cwd() }))
app.listen(3000)
typescript
应用入口(renderer/_app.ts)
import { createSSRApp } from 'vue'
import type { PageContext } from './types'
import { setPageContext } from './usePageContext'
export function createApp(pageContext: PageContext) {
const { Page, pageProps } = pageContext
const app = createSSRApp(Page, pageProps)
// 将页面上下文注入应用
setPageContext(app, pageContext)
return { app }
}
typescript
其中 PageShell 是最外层的 Layout 布局,通过 <slot /> 插槽加载具体页面内容。
集成 Pinia 状态管理
参考官方 Pinia 集成示例,需要改造三个文件:
1. 安装依赖
pnpm add pinia
bash
2. 改造入口文件(_app.ts)
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
export function createApp(pageContext: PageContext) {
const app = createSSRApp(Page, pageProps)
// 创建 Pinia 实例
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
typescript
3. 服务端状态透传(_default.page.server.ts)
// 从 createApp 解构出 pinia
const { app, pinia } = await createApp(pageContext)
// 在 pageContext 中透传初始状态
pageContext.initialPiniaState = pinia.state.value
typescript
4. 客户端状态恢复(client.ts)
// 客户端水合时恢复 Pinia 状态
if (pageContext.initialPiniaState) {
pinia.state.value = pageContext.initialPiniaState
}
typescript
关键提醒:不要忘记在
passToClient中添加initialPiniaState,否则会出现 "initial store state is missing" 错误。
5. 在页面中使用
<script setup>
import { useCounterStore } from '../../stores/useCounter'
const counterStore = useCounterStore()
</script>
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<button @click="counterStore.increment">+1</button>
</div>
</template>
vue
版本升级注意事项
如果遇到 hasInjectionContext 相关的导出错误,说明 Vue 版本过低。需要升级依赖:
# 检查可升级的包
npx npm-check -u
# 重点升级 vue、pinia、vite 等核心依赖
# 注意:typescript 和 @types/node 通常不建议跨大版本升级
bash
集成其他功能
由于项目基于 Vite,之前集成的功能可以复用:
- 自动组件导入:在
vite.config.ts中配置Components插件 - 图标系统:通过
Icons插件支持 Iconify 图标,直接使用<i-mdi-web />等格式 - Markdown 渲染:需要创建
.md.d.ts类型声明文件,并在tsconfig.json中 include - UnoCSS:通过
UnoCSS()插件集成
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import Icons from 'unplugin-icons/vite'
import UnoCSS from 'unplugin-unocss/vite'
export default {
plugins: [
Components({ /* ... */ }),
Icons({ compiler: 'vue3' }),
UnoCSS(),
]
}
typescript
实战总结
通过 vite-plugin-ssr 改造项目的过程让我们理解了 SSR 的核心机制:
- 双端构建:服务端和客户端各自有一套构建流程
- 状态透传:服务端渲染的 Store 状态需要传递给客户端进行水合(Hydration)
- 文件路由:基于
pages/目录自动生成路由配置 - 渐进式集成:原有的 Vite 插件和工具链可以复用,降低迁移成本
这个方案虽然可行,但配置较为繁琐,需要手动处理服务端和客户端的差异。下一节我们将了解 Nuxt.js —— 一个开箱即用的 SSR 框架,可以大幅简化这个过程。
↑